<?php
/**
 * @link https://craftcms.com/
 * @copyright Copyright (c) Pixel & Tonic, Inc.
 * @license https://craftcms.github.io/license/
 */

namespace craft\services;

use Craft;
use yii\base\Exception;
use yii\base\InvalidArgumentException;
use yii\base\InvalidConfigException;
use yii\helpers\Inflector;

/**
 * Security component.
 * An instance of the Security component is globally accessible in Craft via [[\yii\base\Application::getSecurity()|`Craft::$app->security`]].
 *
 * @author Pixel & Tonic, Inc. <support@pixelandtonic.com>
 * @since 3.0.0
 */
class Security extends \yii\base\Security
{
    /**
     * @var string[] Keywords used to reference sensitive data
     * @see redactIfSensitive()
     */
    public $sensitiveKeywords = [];

    /**
     * @var mixed
     */
    private $_blowFishHashCost;

    /**
     */
    public function init()
    {
        parent::init();

        $this->_blowFishHashCost = Craft::$app->getConfig()->getGeneral()->blowfishHashCost;
    }

    /**
     * @return int
     */
    public function getMinimumPasswordLength(): int
    {
        return 6;
    }

    /**
     * Hashes a given password with the bcrypt blowfish encryption algorithm.
     *
     * @param string $password The string to hash
     * @param bool $validateHash If you want to validate the just generated hash. Will throw an exception if
     * validation fails.
     * @return string The hash.
     */
    public function hashPassword(string $password, bool $validateHash = false): string
    {
        $hash = $this->generatePasswordHash($password, $this->_blowFishHashCost);

        if ($validateHash && !$this->validatePassword($password, $hash)) {
            throw new InvalidArgumentException('Could not hash the given string.');
        }

        return $hash;
    }

    /**
     * Deprecated wrapper for [[\craft\config\GeneralConfig::securityKey|Craft::$app->config->general->securityKey]].
     *
     * @return string
     * @deprecated in 3.0.0-beta.27. Use [[\craft\config\GeneralConfig::securityKey|Craft::$app->config->general->securityKey]] instead.
     */
    public function getValidationKey(): string
    {
        return Craft::$app->getConfig()->getGeneral()->securityKey;
    }

    /**
     * @inheritdoc
     * @param string $data the data to be protected
     * @param string|null $key the secret key to be used for generating hash. Should be a secure
     * cryptographic key.
     * @param bool $rawHash whether the generated hash value is in raw binary format. If false, lowercase
     * hex digits will be generated.
     * @return string the data prefixed with the keyed hash
     * @throws Exception if the validation key could not be written
     * @throws InvalidConfigException when HMAC generation fails.
     * @see validateData()
     * @see generateRandomKey()
     * @see hkdf()
     * @see pbkdf2()
     */
    public function hashData($data, $key = null, $rawHash = false): string
    {
        if ($key === null) {
            $key = Craft::$app->getConfig()->getGeneral()->securityKey;
        }

        return parent::hashData($data, $key, $rawHash);
    }

    /**
     * @inheritdoc
     * @param string $data the data to be validated. The data must be previously
     * generated by [[hashData()]].
     * @param string|null $key the secret key that was previously used to generate the hash for the data in [[hashData()]].
     * function to see the supported hashing algorithms on your system. This must be the same
     * as the value passed to [[hashData()]] when generating the hash for the data.
     * @param bool $rawHash this should take the same value as when you generate the data using [[hashData()]].
     * It indicates whether the hash value in the data is in binary format. If false, it means the hash value consists
     * of lowercase hex digits only.
     * hex digits will be generated.
     * @return string the real data with the hash stripped off. False if the data is tampered.
     * @throws Exception if the validation key could not be written
     * @throws InvalidConfigException when HMAC generation fails.
     * @see hashData()
     */
    public function validateData($data, $key = null, $rawHash = false)
    {
        if ($key === null) {
            $key = Craft::$app->getConfig()->getGeneral()->securityKey;
        }

        return parent::validateData($data, $key, $rawHash);
    }

    /**
     * @inheritdoc
     * @param string $data the data to encrypt
     * @param string|null $inputKey the input to use for encryption and authentication
     * @param string $info optional context and application specific information, see [[hkdf()]]
     * @return string the encrypted data
     * @throws InvalidConfigException on OpenSSL not loaded
     * @throws Exception on OpenSSL error
     * @see decryptByKey()
     * @see encryptByPassword()
     */
    public function encryptByKey($data, $inputKey = null, $info = null)
    {
        if ($inputKey === null) {
            $inputKey = Craft::$app->getConfig()->getGeneral()->securityKey;
        }

        return parent::encryptByKey($data, $inputKey, $info);
    }

    /**
     * @inheritdoc
     * @param string $data the encrypted data to decrypt
     * @param string|null $inputKey the input to use for encryption and authentication
     * @param string $info optional context and application specific information, see [[hkdf()]]
     * @return bool|string the decrypted data or false on authentication failure
     * @throws InvalidConfigException on OpenSSL not loaded
     * @throws Exception on OpenSSL error
     * @see encryptByKey()
     */
    public function decryptByKey($data, $inputKey = null, $info = null)
    {
        if ($inputKey === null) {
            $inputKey = Craft::$app->getConfig()->getGeneral()->securityKey;
        }

        return parent::decryptByKey($data, $inputKey, $info);
    }

    /**
     * Checks the given key to see if it looks like it contains sensitive info, and if so, redacts the given value.
     *
     * @param string $name
     * @param string|array $value
     * @return string|array The possibly-redacted value
     */
    public function redactIfSensitive(string $name, $value)
    {
        if (is_array($value)) {
            foreach ($value as $n => &$v) {
                $v = $this->redactIfSensitive($n, $v);
            }
        } else if (
            is_string($value) &&
            preg_match('/\b(' . implode('|', $this->sensitiveKeywords) . ')\b/', Inflector::camel2words($name, false))
        ) {
            $value = str_repeat('•', strlen($value));
        }

        return $value;
    }
}
